package grex.genes;

import grex.Neighbour;
import grex.Options;
import grex.Prediction;
import grex.PredictionContainer;

import java.util.ArrayList;
import java.util.Collections;

public class Knn extends Gene implements ITerminal{
   private double[] support;
   private static final int NROFCILDREN = 1;
   private int k = -1,  classes;
   private double kPercent;
   private PredictionContainer neigbours;
   
 //  private double prob = 0;
   private double lowerProb = -1;
   private double instProb=0;
   private double[] attributeWeights;
   int count =0;
   private boolean initialized=false;
   
   public Knn() {
       super(NROFCILDREN);
       kPercent = Options.rnd.nextDouble();       
   }   

    public synchronized void execute(Gene parent, PredictionContainer data, int mode) throws GeneException {
        
        this.parent = parent;
       if (mode==Gene.TRAIN_MODE) {
           //support=new double[environment.getNrOfTargetCategories()];
           neigbours=data;
           count=data.values().size();           
           if (k == -1) {
               k = (int) (kPercent*(ops.getKNN_MAX()-ops.getKNN_MIN()))+ops.getKNN_MIN();
               if (k % 2 == 0) {
                   k++;
               }
           }
       }
       for (Prediction p : data.values()) {
           support = new double[environment.getNrOfTargetCategories()];
           p.setSupport(support);
           
           p.setPrediction(calcValue(p.getInstance()));
           p.setLeaf(this);
       }

   }

   private double calcValue(double[] instance) throws GeneException {
      ArrayList<Neighbour> knns;
       if(ops.isKNN_WEIGHTED_ATTRIBUTES()||ops.isKNN_ATTRIBUTES_SELECTION()){
           attributeWeights =((KnnAttWeights) this.getChildren()[0]).getAttributeWeights();
           knns = getNearestAttrKnns(instance, ops.getNR_OF_CONSIDERED_NEIGBHOURS());           
       }else
             knns = environment.getKNNs(instance, k);
       double classValue = -666;
       if (knns.size() != 0) {
           if(ops.getPROBLEM_TYPE()==ops.CLASSIFICATION){
               calcSupport(knns);
               return -1;//classValue;
           }else{
               double avgPred=0;
               if(k>knns.size())
                   k=knns.size();
               for(int i = 0; i < k ;i++){
                   double[] current = knns.get(i).getInstance();
                   avgPred+=current[current.length-1]/k;
               }
               instProb=0;
               lowerProb=0;
               int lowerCount=0;
               int probCount=0;
               for(int i = 0; i < k;i++){
                   double[] current = knns.get(i).getInstance();               
                   double target = current[current.length-1];
                   if(avgPred>target){
                        instProb+=(Math.abs(target-avgPred)/target);                   
                        probCount++;
                   }else{
                        lowerProb+=(Math.abs(target-avgPred)/target);                   
                        lowerCount++;
                   }
               }
               if(probCount>0)
               instProb=instProb/probCount;
               if(lowerCount>0)
               lowerProb=lowerProb/lowerCount;
               
               return avgPred;
           }
       }
       return -666;
   }

   private void calcSupport(ArrayList<Neighbour> knns) {
       //calc class Distribution
       double targetValue;
       double realNeighbours = 0;
       ArrayList<Neighbour> arlRealNeigbours  = new ArrayList<Neighbour>();
       for (int i = 0; realNeighbours < k && i < knns.size(); i++) {
     //      if (!knns.get(i).getFold().equalsIgnoreCase(ops.getTestFOLD())) {
               arlRealNeigbours.add(knns.get(i));
               double[] n = knns.get(i).getInstance();
               realNeighbours++;
               targetValue = n[n.length - 1];
               support[(int)targetValue]++;
    //      }
       }
       if( ops.isKNN_WEIGHTED_VOTING())
         calcWeightedSupport(arlRealNeigbours);
   }

   private void calcWeightedSupport(ArrayList<Neighbour> knns) {
       for(int i = 0;i<support.length;i++)
           support[i]=0;
       
       double target=0,totDist=0;
       for(Neighbour kn: knns)
           totDist+=kn.getDistance();
       for (int i = 0; i < k && i < knns.size(); i++) {
           Neighbour kn = knns.get(i);
           double[] n = kn.getInstance();
           target= n[n.length-1];
           int targetClass = (int)target;
           if(totDist!=0)
             support[targetClass]+=(1-(kn.getDistance()/totDist));
       }                 
   }
      private ArrayList<Neighbour> getNearestAttrKnns(double[] instance,int nrOfConsideredNeigbours) {
       ArrayList<Neighbour> knns = environment.getKNNs(instance, nrOfConsideredNeigbours);

       ArrayList<Neighbour> consideredKnns = new ArrayList<Neighbour>();
       for(int z = 0; z < nrOfConsideredNeigbours && z<knns.size();z++){
           Neighbour k =  knns.get(z).clone();
           consideredKnns.add(k);
           double[] n = k.getInstance();
           //Calc weighted distance
           double attrDist = 0;
           for(int i = 0;i < attributeWeights.length; i++){
               if(environment.isNumericColumn(i))
                    attrDist += Math.pow(instance[i]-n[i],2)*attributeWeights[i]; 
               else{//Categorical attribute
                   if (instance[i] != n[i]) {
                        attrDist += 1 * attributeWeights[i];
                    }
               }
           }
           k.setDistance(Math.sqrt(attrDist));
       }
       Collections.sort(consideredKnns);
       return consideredKnns;
 
   }
   

   public double getKPercent() {
       return kPercent;
   }

   public void setKPercent(double kPercent) {
       this.kPercent = kPercent;
   }

    public double getLowerProb() {
        return lowerProb;
    }

   public void setNeigbours(PredictionContainer neigbours) {
       this.neigbours = neigbours;
   }

   public PredictionContainer getNeigbours() {
       return neigbours;
   }

   public void setProb(double prob){
       this.instProb=prob;
   }

   public void setK(int k){
       this.k=k;
   }
public void setCount(int count){
           this.count=count;
       }

   public Object clone() {
       Knn newKnn;
       newKnn = new Knn();
       newKnn.neigbours=neigbours.shallowClone();
       newKnn.ops = ops;  
       newKnn.environment = environment;
       newKnn.count = count;
       newKnn.instProb=instProb;
       newKnn.lowerProb=lowerProb;
       newKnn.k=k;
       newKnn.parent = parent;
       cloneChildren(newKnn); 
       return newKnn;
   }

   public String toString() {
       String s =  "KNN" + k + "(" + neigbours.size() + ")";      
       return s;
   }

   public double getProb() {
      return instProb;
   }

   public int getCount() {
       return count;
   }

    public double getLowProb() {
        return lowerProb;
    }

    public boolean isInitialized() {
        return initialized;
    }

    public void setInitialized(boolean initialized) {
        this.initialized = initialized;
    }
    public void initAttributesWeights(){
        attributeWeights = new double[ops.getNR_OF_INPUT_COLUMNS()];
        for(int i=0;i<attributeWeights.length;i++)
            attributeWeights[i]= ops.rnd.nextDouble();
    }

    public double[] getAttributeWeights() {
        return attributeWeights;
    }

    public void setAttributeWeights(double[] attributeWeights) {
        this.attributeWeights = attributeWeights;
    }

    public double getPrediction() {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    public double[] getProbs() {
        throw new UnsupportedOperationException("Not supported yet.");
    }
}
